import Callbacks from "@/utils/Callbacks";
import localforage from "localforage";
export interface ITaskInfo<T extends string = string, V extends {} = {}> {
  id?: string;
  type: T;
  title?: string;
  desc?: string
  data: V;
  current?: number;
  total?: number;
  push_time?: number;
  start_time?: number;
  finish_time?: number;
  retry_time?: number;
  status?: 'resolved' | 'rejected' | 'waiting' | 'pending',
  resaon?: any;
}
export interface ITask<T extends string = string, V extends {} = {}> extends ITaskInfo<T, V> {
  run(): Promise<unknown>
}
export interface IEnsuredTask<T extends string = string, V extends {} = {}> extends Required<ITask<T, V>> {
}

const forage = localforage.createInstance({ name: 'tasks' })
export interface ITasksKeeperCallbacks<T extends string = string, D extends { [k in T]: any } = { [k in T]: any }> {
  on_task_change?(owner: TasksKeeper<T, D>, task: IEnsuredTask<T, D[T]>, prev: IEnsuredTask<T, D[T]>): void;
  on_add?(owner: TasksKeeper<T, D>, task: IEnsuredTask<T, D[T]>): void;
  on_start?(owner: TasksKeeper<T, D>, task: IEnsuredTask<T, D[T]>): void;
  on_resolve?(owner: TasksKeeper<T, D>, task: IEnsuredTask<T, D[T]>, result: any): void;
  on_reject?(owner: TasksKeeper<T, D>, task: IEnsuredTask<T, D[T]>, result: any): void;
  on_finish?(owner: TasksKeeper<T, D>, task: IEnsuredTask<T, D[T]>): void;
  on_progress?(owner: TasksKeeper<T, D>, task: IEnsuredTask<T, D[T]>, current: number, total: number): void;
  on_type_add?(owner: TasksKeeper<T, D>, type: string): void;
  on_auto_submit_change?(owner: TasksKeeper<T, D>, auto_submit: boolean): void;
  on_auto_collect_change?(owner: TasksKeeper<T, D>, auto_collect: boolean): void;
  on_too_full_change?(owner: TasksKeeper<T, D>, too_full: boolean): void;
}

export type ITaskDataMap<T extends string> = { [k in T]: any }
export type ITaskCreatorMap<T extends string = string, D extends ITaskDataMap<T> = ITaskDataMap<T>> = { [k in T]: (data: D[k]) => ITask<k, D[k]> }

export class TasksKeeper<T extends string = string, D extends { [k in T]: any } = { [k in T]: any }> {
  static readonly TAG: string = 'TasksKeeper';
  static create<
    T extends string = string,
    D extends ITaskDataMap<T> = ITaskDataMap<T>,
    C extends ITaskCreatorMap<T, D> = ITaskCreatorMap<T, D>
  >(creators: C) {
    return new TasksKeeper<T, D>(creators)
  }

  readonly TAG = TasksKeeper.TAG;
  readonly callbacks = new Callbacks<ITasksKeeperCallbacks<T, D>>();

  private _task_creators: { [k in T]: (data: D[k]) => ITask<T, D[k]> }
  private _types = new Set<string>()
  private _waitings: IEnsuredTask<T, D[T]>[] = []
  private _penddings: IEnsuredTask<T, D[T]>[] = [];
  private _max_executions: number = 5;
  private _gap_time: number = 10;
  private _auto_submit = false;
  private _auto_collect = true;
  private _is_ready = false;
  private _is_initing = false;
  private _init_callbacks: [() => void, () => void][] = []
  private _too_full = false;
  private _too_full_num = 5000;
  get too_full(): boolean { return this._too_full }
  constructor(creators: { [k in T]: (data: D[k]) => ITask<T, D[k]> }) {
    this._task_creators = creators;
    this.init()
  }
  protected init(): Promise<void> {
    if (this._is_ready) return Promise.resolve();
    if (this._is_initing) {
      return new Promise<void>((a, b) => this._init_callbacks.push([a, b]))
    }

    this._is_initing = true;
    const time = Date.now();
    return new Promise<void>(async (resolve) => {
      const keys = await forage.keys()
        .catch(e => { console.error(`[${this.TAG}::init] failed to get keys`, e); return [] })
      for (const key of keys) {
        const task = await forage.getItem<ITaskInfo<T, D[T]>>(key)
          .catch(e => {
            console.error(`[${this.TAG}::init] task lost, task by key "${key}"`, e);
            return null
          });
        if (!task) continue;

        const task_creator = this._task_creators[task.type];
        if (!task_creator) {
          console.error(`[${this.TAG}::init] task lost, task creator by type "${task.type}"`);
          continue;
        }
        if (!task.id) {
          console.error(`[${this.TAG}::init] task lost, task id got ${task.id}`);
          continue;
        }
        const info = task_creator(task.data)
        const ensured_task = this.ensure_task(info, task.id)
        this.waitings.push(ensured_task)
      }
      this._is_ready = true;
      this._is_initing = false;
      resolve();
      for (const [resolve] of this._init_callbacks)
        resolve();
    })
  }

  get waitings(): IEnsuredTask<T, D[T]>[] { return this._waitings }
  get penddings(): IEnsuredTask<T, D[T]>[] { return this._penddings }
  set max_executions(v: number) { this._max_executions = Math.max(1, Math.floor(v)) }
  get max_executions(): number { return this._max_executions }
  set gap_time(v: number) { this._gap_time = Math.min(0, Math.floor(v)) }
  get gap_time(): number { return this._gap_time }
  get task_types(): string[] { return Array.from(this._types) }
  get auto_submit() { return this._auto_submit };
  set auto_submit(v: boolean) {
    if (this._auto_submit !== v) {
      this._auto_submit = v;
      this.callbacks.emit('on_auto_submit_change')(this, v)
    }
    if (v) this.run();
  }
  get auto_collect() { return this._auto_collect };
  set auto_collect(v: boolean) {
    if (this._auto_collect !== v) {
      this._auto_collect = v;
      this.callbacks.emit('on_auto_collect_change')(this, v)
    }
  }
  private retry_time_map = new Map<string, number>();
  update_progress(job: IEnsuredTask<T, D[T]>, current: number, total: number) {
    job.current = current;
    job.total = total
    this.callbacks.emit('on_progress')(this, job, current, total)
  }
  private _id = 1;
  del(id: string) {
    let idx = this._waitings.findIndex(v => v.id === id)
    if (idx >= 0) this._waitings.splice(idx, 1)
    idx = this._penddings.findIndex(v => v.id === id)
    if (idx >= 0) this._penddings.splice(idx, 1)
    forage.removeItem(id)
  }
  async start(id: string) {
    await this.init()
    const idx = this._waitings.findIndex(v => v.id === id)
    if (idx < 0) return false
    const [task] = this._waitings.splice(idx, 1)
    this._start(task)
  }
  insert(task: ITask<T, D[T]>, index: number): string {
    // console.log(`[${this.TAG}::insert]`)
    const task_id = task.id ?? ('task_' + (++this._id))
    this.init().then(() => {
      // console.log(`[${this.TAG}::insert] ready, add it to waitings`)
      const exists_idx = this._waitings.findIndex(v => v.id === task_id);
      const exists_task = this._waitings[exists_idx];
      const ensured_task: IEnsuredTask<T, D[T]> = this.ensure_task(task, task_id)
      this.add_type(ensured_task)
      if (exists_task) {
        const prev = { ...exists_task }
        Object.assign(exists_task, ensured_task)
        this._waitings.splice(exists_idx, 1); // move it
        this._waitings.splice(index, 0, exists_task);
        this.callbacks.emit('on_task_change')(this, exists_task, prev)
        this.run();
        forage.removeItem(ensured_task.id)
          .catch(e => console.error(e))
          .then(() => forage.setItem(ensured_task.id, { ...ensured_task, run: void 0 }))
          .catch(e => console.error(e))
        const too_full = this._penddings.length + this._waitings.length > this._too_full_num
        if (too_full != this._too_full) this.callbacks.emit("on_too_full_change")(this, this._too_full = too_full)
      } else {
        forage.setItem(ensured_task.id, { ...ensured_task, run: void 0 }).catch(e => console.error(e))
        this._waitings.splice(index, 0, ensured_task);
        this.callbacks.emit('on_add')(this, ensured_task)
        this.run();
      }
    }).catch(e => {
      console.log(`[${this.TAG}::insert] failed`, e)
    })
    return task_id
  }
  private ensure_task(task: ITask<T, D[T]>, task_id: string): IEnsuredTask<T, D[T]> {
    return {
      ...task,
      id: task_id,
      title: task.title || 'unnamed job',
      desc: task.desc || 'unnamed job',
      data: task.data,
      current: 0,
      total: task.total || 0,
      push_time: Date.now(),
      start_time: 0,
      finish_time: 0,
      retry_time: this.retry_time_map.get(task_id) || 0,
      status: 'waiting',
      resaon: void 0,
    };
  }

  push(task: ITask<T, D[T]>): string {
    return this.insert(task, this._waitings.length)
  }
  unshift(task: ITask<T, D[T]>): string {
    return this.insert(task, 0)
  }

  protected add_type(info: Required<ITaskInfo<T, D[T]>>) {
    const l1 = this._types.size;
    this._types.add(info.type);
    if (l1 != this._types.size) this.callbacks.emit('on_type_add')(this, info.type);
  }

  protected run(): void {
    if (this._penddings.length >= this.max_executions || !this._waitings.length || !this.auto_submit) return;
    const task = this._waitings.shift();

    const too_full = this._penddings.length + this._waitings.length > this._too_full_num
    if (too_full != this._too_full) this.callbacks.emit("on_too_full_change")(this, this._too_full = too_full)

    if (!task) return;
    this._start(task);
    this.run();
  }

  private _start(task: IEnsuredTask<T, D[T]>) {
    this._penddings.push(task);
    this.callbacks.emit('on_start')(this, task);
    task.start_time = Date.now();
    task.status = 'pending';
    task.run().then((r) => {
      task.status = 'resolved';
      this.retry_time_map.delete(task.id);
      this.callbacks.emit('on_resolve')(this, task, r);
      forage.removeItem(task.id).catch(e => console.error(e))
      if (this._gap_time) return new Promise(r => setTimeout(r, this._gap_time));
    }).catch(r => {
      task.resaon = r;
      task.status = 'rejected';
      console.warn(`[${TasksKeeper.TAG}] rejected, reason:`, r, `task:`, task)
      const time = this.retry_time_map.get(task.id) || 0;
      this.retry_time_map.set(task.id, time + 1);
      this.callbacks.emit('on_reject')(this, task, r);
      if (this._gap_time) return new Promise(r => setTimeout(r, this._gap_time));
    }).finally(() => {
      task.finish_time = Date.now();
      const idx = this._penddings.indexOf(task);
      if (idx < 0) console.error(`[${TasksKeeper.TAG}] task no exists in _pendings? why?`, task);
      else this._penddings.splice(idx, 1);
      this.callbacks.emit('on_finish')(this, task);
      this.run();
    });
  }
}
